Skip to content

Conversation

@MarcelOlsen
Copy link

@MarcelOlsen MarcelOlsen commented Nov 6, 2025

Problem

Guard schemas defined via guard() do not appear in the generated OpenAPI specification. Routes exist but lack requestBody and parameter schemas.

Current Behavior

app.guard({
  body: t.Object({
    username: t.String(),
    password: t.String()
  })
}, (app) =>
  app
    .post('/sign-up', ({ body }) => body)
    .post('/sign-in', ({ body }) => body)
)

OpenAPI output:

{
  "/sign-up": {
    "post": {
      "operationId": "postSign-up"
      // No requestBody!
    }
  }
}

Root Cause

The plugin reads routes via app.getGlobalRoutes(), which returns guard schemas in standaloneValidator arrays. The plugin only checks direct schema properties like route.hooks.body.

Solution

Implement local schema flattening directly in elysia-openapi. This approach:

  • Merges standaloneValidator arrays into direct hook properties
  • Handles all schema types: body, query, headers, params, cookie, response
  • Properly handles TypeBox unions, intersects, and other complex schemas
  • Enables future support for external schema conversions (z.toJSONSchema, etc.)

Implementation

Added helper functions to src/openapi.ts:

  • flattenRoutes() - Main function that flattens guard schemas
  • mergeStandaloneValidators() - Merges standaloneValidator arrays
  • mergeSchemaProperty() - Merges body/query/headers/params/cookie schemas
  • mergeResponseSchema() - Handles response schemas and status code objects
  • isTSchema() - Distinguishes TypeBox schemas from status code objects using Kind symbol
  • normalizeSchemaReference() - Converts string references to TRef nodes
  • mergeObjectSchemas() - Merges TypeBox object schemas

File: src/openapi.ts, line ~628

// Before
const routes = app.getGlobalRoutes()

// After
// Flatten routes to merge guard() schemas into direct hook properties
const routes = flattenRoutes(app.getGlobalRoutes())

Why Local Implementation?

Per @SaltyAom feedback, implementing schema flattening in elysia-openapi (rather than elysia core) allows:

  1. Better separation of concerns - OpenAPI-specific logic stays in the OpenAPI plugin
  2. Support for external schema libraries like Zod, Valibot, etc. via mapJsonSchema
  3. No need to maintain protected methods in elysia core for plugin access

After Fix

OpenAPI output:

{
  "/sign-up": {
    "post": {
      "requestBody": {
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "username": { "type": "string" },
                "password": { "type": "string" }
              },
              "required": ["username", "password"]
            }
          }
        },
        "required": true
      },
      "operationId": "postSign-up"
    }
  }
}

Testing

✅ Added comprehensive tests in test/guard-schema.test.ts:

  • Guard headers appear in OpenAPI spec
  • Guard response schemas merge with route-level schemas
  • All schema types (body, query, params, headers, cookie, response) are handled
  • TypeBox unions, intersects, and complex schemas work correctly

The fix has been tested with:

  • Basic guard() schemas
  • Nested guards
  • Mixed guard + direct schemas
  • String schema references from .model()
  • Status code objects in response schemas

All schemas now appear correctly in the OpenAPI documentation.

… spec

Fixes guard() schemas not appearing in generated OpenAPI documentation.

Changes:
- Replace app.getGlobalRoutes() with app.getFlattenedRoutes?.() ?? app.getGlobalRoutes()
- Maintains backward compatibility with older Elysia versions

This allows the OpenAPI plugin to access guard() schemas that are now
exposed via Elysia's getFlattenedRoutes() method.

Requires: elysiajs/elysia#1533
@coderabbitai
Copy link

coderabbitai bot commented Nov 6, 2025

Walkthrough

Adds schema-flattening helpers and a route-flattening mechanism to merge guard() schemas (body, query, headers, params, cookie, responses) into route hook properties, and updates OpenAPI generation to use flattened routes before producing the final OpenAPI document.

Changes

Cohort / File(s) Change Summary
OpenAPI schema & flattening logic
src/openapi.ts
Added schema-normalization and merging helpers (mergeObjectSchemas, isTSchema, normalizeSchemaReference, mergeSchemaProperty, mergeResponseSchema, mergeStandaloneValidators), implemented flattenRoutes to merge guard() schemas into route hooks and normalize string refs to TypeBox references; switched route retrieval to use flattened routes in toOpenAPISchema. Extended imports to include TObject from @sinclair/typebox.
Tests for guard schema handling
test/guard-schema.test.ts
New tests verifying OpenAPI output when guard schemas are used: ensures guarded POST /users exposes requestBody and required header parameter, and that GET /data merges guard-level 401/500 responses with route-level 200 response.

Sequence Diagram(s)

sequenceDiagram
    participant App as ElysiaApp
    participant OpenAPI as toOpenAPISchema
    participant Flat as flattenRoutes
    participant Route as RouteItem
    note over App,Flat: New flattening step merges guard() schemas into routes

    App->>Flat: getGlobalRoutes() / input routes
    Flat-->>OpenAPI: flattened routes (merged body/query/headers/params/cookie/responses)
    OpenAPI->>OpenAPI: normalize references, convert string refs -> TypeBox refs
    OpenAPI->>Caller: final OpenAPI JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas to focus:
    • flattenRoutes merging semantics for body/query/headers/params/cookie and response maps (status-code handling).
    • Correctness of reference normalization (string -> TypeBox ref) and safety of detect/isTSchema logic.
    • Effects on OpenAPI generation paths in toOpenAPISchema and any assumptions about route shapes.
    • New tests: ensure they cover edge cases and reflect intended merging order.

Poem

🐇 I hop through schemas, twining guard with route,

I stitch body, headers, responses — a tiny pursuit.
With TypeBox threads and a flattened song,
I hum the spec together, neat and strong. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title uses a wrench emoji and includes 'fix:' prefix that don't align with standard PR title conventions; the technical substance ('use getFlattenedRoutes() to include guard() schemas') is relevant but the presentation is unconventional. Consider removing the wrench emoji and 'fix:' prefix, then use a clear conventional title like 'Include guard() schemas in OpenAPI spec generation' for better clarity and consistency.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MarcelOlsen MarcelOlsen marked this pull request as draft November 6, 2025 22:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 070919e and 9095926.

📒 Files selected for processing (1)
  • src/openapi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/openapi.ts (2)
example/gen.ts (1)
  • app (6-77)
test/gen/sample.ts (1)
  • app (5-61)

MarcelOlsen added a commit to MarcelOlsen/elysia-openapi that referenced this pull request Nov 7, 2025
   fix: use @ts-expect-error instead of @ts-ignore

   Addresses coderabbitai feedback on PR elysiajs#300.

   Changed from @ts-ignore to @ts-expect-error for better type safety.
   This ensures the suppression is still necessary - if the error goes
   away in the future (e.g., after types are updated), @ts-expect-error
   will fail the build, alerting us to remove it.

   Also improved the comment to be more descriptive about why the
   suppression is needed.
   Addresses coderabbitai feedback on PR elysiajs#300.

   Changed from @ts-ignore to @ts-expect-error for better type safety.
   This ensures the suppression is still necessary - if the error goes
   away in the future (e.g., after types are updated), @ts-expect-error
   will fail the build, alerting us to remove it.

   Also improved the comment to be more descriptive about why the
   suppression is needed.
@MarcelOlsen MarcelOlsen force-pushed the fix/use-flattened-routes-for-guard-schemas branch from 1fcdb20 to da02e4d Compare November 7, 2025 09:31
@MarcelOlsen
Copy link
Author

MarcelOlsen commented Nov 7, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai
Copy link

coderabbitai bot commented Nov 7, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

  - Add flattenRoutes() and schema merging helpers to elysia-openapi
  - Replace app.getFlattenedRoutes() call with local implementation
  - Use app.getGlobalRoutes() and flatten routes internally
  - Enables guard() schemas to appear in OpenAPI documentation
  - Allows future support for external schema conversions (z.toJSONSchema,
  etc.)

  This moves the flattening logic from elysia core to the OpenAPI plugin
  where it belongs, as suggested by the maintainer.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/openapi.ts (2)

266-330: Response merging works for TypeBox/status maps but may misclassify ~standard schemas

The mergeResponseSchema + isTSchema combo correctly handles the common cases (TypeBox schema vs { 401: ... } status maps, including your new guard-response test). However, any non-TypeBox object (including ~standard vendor schemas such as Zod/Valibot) is currently treated as “not a schema” and funneled through the same path as status-code maps.

That means scenarios like:

  • route-level response: t.Object(...) (TypeBox),
  • guard-level response: someZodSchema (non-status, ~standard),

will be merged as if the guard schema were a status-map container, which is likely not what you want and can produce awkward shapes that later go through the single-schema response branch.

Consider tightening the detection so that “plain schemas” include both TypeBox (Kind in value) and ~standard shapes, and only treat objects without either as status maps. That keeps your current behavior for { 401: ... } but avoids misclassifying standalone non-TypeBox schemas. A small regression test for guard({ response: someZodSchema }, ...) would make this explicit.


335-434: Route flattening for guards looks good; consider clarifying reliance on local vs core flattening

The mergeStandaloneValidators + flattenRoutes(app.getGlobalRoutes()) approach is a clean way to surface guard() schemas to the existing OpenAPI generation logic, and keeping this as a pure transform (cloning the route object when standaloneValidator is present) avoids mutating app.routes.

One thing to double‑check is alignment with expectations/documentation: the PR description still talks about calling app.getFlattenedRoutes?.() ?? app.getGlobalRoutes(), but the implementation now always uses getGlobalRoutes() and does its own flattening. That’s fine from a compatibility standpoint, but it would be good either to:

  • update the PR/docs to match this behavior, or
  • optionally prefer app.getFlattenedRoutes?.() when available and fall back to flattenRoutes(getGlobalRoutes()), to reduce drift from core semantics.

Functionally the current code is sound; this is mostly about keeping intent and implementation in sync.

Also applies to: 626-628

test/guard-schema.test.ts (1)

1-88: Nice focused regression coverage for guard headers and responses

These tests exercise the key failure modes that motivated the change (guard headers missing from parameters and guard responses not merged with route-level 200s) by going through the real /openapi/json endpoint. This is a solid baseline. If you later extend coverage, adding cases for guard-driven query/params/cookie schemas or nested guards would broaden confidence, but not required for this fix.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da02e4d and e986f1b.

📒 Files selected for processing (2)
  • src/openapi.ts (3 hunks)
  • test/guard-schema.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
test/guard-schema.test.ts (1)
src/index.ts (1)
  • openapi (40-181)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant